Join 運算子的第四篇文章,將和大家分享如何利用 GroupJoin 運算子完成 Left outer join 的效果。
自學筆記這系列是我自己學習的一些心得分享,歡迎指教。這系列的分享,會以 C# + 我比較熟的 Net 3.5 環境為主。
另外本系列預計至少會切成【打地基】和【語法應用】兩大部分做分享。打地基的部分,講的是 LINQ 的組成元素,這部分幾乎和 LINQ 無關,反而是 C# 2.0、C# 3.0 的一堆語言特性,例如:型別推斷、擴充方法、泛型、委派等等,不過都會把分享的範圍限制在和 LINQ 應用有直接相關功能。
PS. LINQ 自學筆記幾乎所有範例,都可直接複製到 LINQPad 4 上執行(大多是用 Statements 和 Program 模式)。因為它輕巧好用,功能強大,寫範例很方便,請大家自行到以下網址下載最新的 LINQPad:http://www.LINQpad.net/。
上一篇我們說明了 GroupJoin 運算子,接下我們要利用這個運算子來完成仿 SQL Script 的 Left outer join (左外部聯結)效果。But,人生最機車的就是這個 But,在看 Code 之前,我們要再多了解一個運算子:DefaultIfEmpty。
public static IEnumerable<TSource> DefaultIfEmpty<TSource>(
this IEnumerable<TSource> source
)
public static IEnumerable<TSource> DefaultIfEmpty<TSource>(
this IEnumerable<TSource> source,
TSource defaultValue
)
DefaultIfEmpty 運算子是 IEnumerable<TSource> 的擴充方法,呼叫它時,會回傳 IEnumerable<TSource>,來源和結果看起來都一樣,唯一的差異是 DefaultIfEmpty 運算子所回傳的序列,其中若有項目是 null,則會以該項目型別(TSource)的預設值帶入,或者我們可以利用第二個多載方法自訂項目為 null 時的預設值。
我們應該了解,外部聯結(Outer join)的意義就是,只要 outer 有的項目,不管在 inner 中是否能對應到,都必須呈現在輸出結果中,換言之,outer 中的項目,對應到在 inner 中有可能是 null,但是 null 是無法變成輸出結果,會發生 NullReferenceException 錯誤,因此我們必須透過 DefaultIfEmpty 運算子幫我們處理 TInner 為 null 時,該怎麼定義 TInner 的輸出結果。
大家可能會問,那有沒有 Right outer join?在 LINQ 裡,沒有!請使用 Left outer join 吧。
But,是的,人生最機車的 But 又出現了!.Net 3.5 的 LINQ to SQL/Entities 完全不支援 DefaultIfEmpty 運算子,.Net 4.0/4.5 的 LINQ to SQL 只支援第一個無參數的多載方法,至於 LINQ to Entities ,MSDN 文件上是寫兩個多載方法都支援,不過實際在 .Net 4.0 上,使用第二個多載方法還是會出現 NotSupportedException 例外,原因我還沒查出來,有結果再回補文章,總之,使用時請留意,才不會老是發生 NotSupportedException 錯誤。
下面範例我們沿用上一篇的 BOM 和 Equip 資料來源,因此程式碼會省略資料來源的定義,請自行翻閱前一篇文章之程式碼,以建立資料來源:
void Main()
{
var BOMTable = DataProvider.getBOM();
var Equips = DataProvider.getEquips();
var query =
from b in BOMTable
join e in Equips
on new { b.BomType, b.WUC } equals new { e.BomType, e.WUC } into be
from obj in be.DefaultIfEmpty()
select new
{
b.BomType, b.WUC,
Name = obj == null ? "N/A" : obj.Name
};
query.Dump("Left outer join query");
}
上述程式碼,我們使用 GroupJoin 運算子聯結 BOMTable 和 Equips 兩個序列,設定用 BomType 和 WUC 屬性做相等比較,到目前為止都還是內部聯結(Inner join),接著我們對 GroupJoin 後的結果序列(be)調用 DefaultIfEmpty 運算子,使用無參數的第一個多載方法,然後在投影(Select)輸出建立匿名型別的 Name 屬性時,判斷若 be 中的項目為 null,就輸出字串 "N/A",否則就輸出項目的 Name 屬性值,至此,完成 Left outer join 效果。
我們看一下對等的方法架構查詢:
BOMTable
.GroupJoin (
Equips,
b =>
new
{
BomType = b.BomType,
WUC = b.WUC
},
e =>
new
{
BomType = e.BomType,
WUC = e.WUC
},
(b, be) =>
new
{
b = b,
be = be
}
)
.SelectMany (
temp0 => temp0.be.DefaultIfEmpty (),
(temp0, obj) =>
new
{
BomType = temp0.b.BomType,
WUC = temp0.b.WUC,
Name = (obj == null) ? "N/A" : obj.Name
}
)
如果改成查詢資料庫,則會產出的 SQL Script 如下:
SELECT
[Extent1].[BomType] AS [BomType],
[Extent1].[WUC] AS [WUC],
CASE WHEN ([Extent2].[Name] IS NULL) THEN N'N/A' ELSE [Extent2].[Name] END AS [C1]
FROM [dbo].[BOMTable] AS [Extent1]
LEFT OUTER JOIN [dbo].[Equip] AS [Extent2] ON ([Extent1].[BomType] = [Extent2].[BomType]) AND ([Extent1].[WUC] = [Extent2].[WUC])
看到囉,產出的 SQL Script 中,有 Left outer join 喔!
接著,看幾個第二個多載方法的範例,一樣可以完成 Left outer join 效果:
void Main()
{
var BOMTable = DataProvider.getBOM();
var Equips = DataProvider.getEquips();
var query =
from b in BOMTable
join e in Equips
on new { b.BomType, b.WUC } equals new { e.BomType, e.WUC } into be
from obj in be.DefaultIfEmpty(new Equip {Name = default(string)})
select new
{
b.BomType, b.WUC,
Name = obj.Name ?? "N/A"
};
query.Dump("Left outer join query");
}
輸出結果和前一個範例是一樣的,就不重複貼圖佔版面了。上述程式碼,改用第二個多載方法,呼叫 DefaultIfEmply 運算子帶入一個新的 Equip 型別,只設定 Name 屬性的預設值,然後投影輸出時,改用 ?? 運算子(Null 結合)處理輸出的 Name 屬性值。
對應的方法架構查詢:
BOMTable
.GroupJoin (
Equips,
b =>
new
{
BomType = b.BomType,
WUC = b.WUC
},
e =>
new
{
BomType = e.BomType,
WUC = e.WUC
},
(b, be) =>
new
{
b = b,
be = be
}
)
.SelectMany (
temp0 =>
temp0.be
.DefaultIfEmpty (
new Equip()
{
Name = null
}
),
(temp0, obj) =>
new
{
BomType = temp0.b.BomType,
WUC = temp0.b.WUC,
Name = (obj.Name ?? "N/A")
}
)
第三種應用 DefaultIfEmpty 運算子的方式,是在投影輸出時使用,請見下述範例:
void Main()
{
var BOMTable = DataProvider.getBOM();
var Equips = DataProvider.getEquips();
var query =
from b in BOMTable
join e in Equips
on new { b.BomType, b.WUC } equals new { e.BomType, e.WUC } into be
select be.DefaultIfEmpty(new Equip
{
BomType = b.BomType,
WUC = b.WUC,
Name = "N/A"
});
query.SelectMany(q => q).Dump("Left outer join query");
}
輸出結果亦同前面的範例。上述程式碼,把 DefaultIfEmpty 運算子放到投影時輸出,但是因為它會回傳一個序列,所以輸出結果其實是一個集合再包一個集合,所以最後我用 SelectMany 做扁平化處理,讓輸出結果和前面的範例一致。